Fix blank code blocks in assistant - #5772
Conversation
A code block's arguments are recomputed on every invalidation of the room resource, which during streaming arrives continuously. `modify` treated each one as a reason to start over: it cleared any error and performed the load again, and because that load is a restartable task, performing it cancelled the request in flight. Two things followed. The resource sat holding no code and no error, which is the one state the template renders as nothing at all — a code block with a header, an editor and an apply button in the DOM but no content in any of them. And since cancelling the task does not cancel the request behind it, every restart left its fetch running and started another, so a patch creating a new file — where a 404 is the expected answer — asked the realm for that file thousands of times. The requests then contended with each other, so each load took longer, so more of them were cancelled before they could finish. Restart only when the file or the patch actually changes; otherwise leave a load that is already under way alone. Give the load an abort signal so a superseded one stops rather than merely having its answer discarded, and thread that signal through getSource. Keep the in-flight flag untracked, since consuming tracked state in `modify` would re-enter it on every change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 1h 37m 16s ⏱️ + 1h 31m 55s Results for commit 3391d1d. ± Comparison against earlier commit ca418d6. Realm Server Test Results 1 files ±0 1 suites ±0 15m 50s ⏱️ + 3m 27s Results for commit 3391d1d. ± Comparison against earlier commit ca418d6. |
Rendering it for any truthy file URL reached the header with values that are not URLs — the model names the file it is patching, and what it writes is not always parseable. `fileName` constructs a URL from it unguarded, so the getter threw and took the whole message render down with it. A patch that failed does not need the header anyway: the footer's alert already speaks for it, and the empty block this was meant to fix is the pending one. Restrict the header to that case, and stop `fileName` from throwing so a header that is merely wrong cannot break the render. The fallback matters beyond this branch — the standard code block renders the same header for any truthy file url. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three behaviours shipped without tests, each one the kind that breaks quietly. A superseded load has to abort its request, since cancelling the task leaves the fetch running and only discards the answer — the difference between a bounded restart and a request storm is invisible from the rendered output. A patch whose diff has not arrived has to say so. Asserting only that the loading state is absent once settled would pass just as well if it never rendered at all, which is the bug this was written to prevent. And a file name that is not a URL has to reach the header without throwing. The model names the file it is patching and what it writes is not always parseable; constructing a URL from it unguarded took down the whole message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe6879a19f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.abortController = abortController; | ||
| this.loadInFlight = true; | ||
| try { | ||
| await this.loadDiff(abortController.signal); |
There was a problem hiding this comment.
Keep cancellable work in the task body
When the inputs change after getSource resolves but while ApplySearchReplaceBlockTool.execute() is awaiting its module load, cancelling this restartable task stops only the outer task at this await; loadDiff() is a separate ordinary async operation, and the abort signal no longer stops it after the fetch. The superseded operation can therefore later overwrite originalCode, modifiedCode, or errorMessage belonging to the newer patch (and can also mutate after destruction), displaying or copying a stale diff. Keep the state-mutating awaits directly in the task or verify the controller/signal after every await before assigning resource state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
[Claude Code 🤖] Real bug, and precisely diagnosed. Fixed in f3d3f89.
You're right about the boundary: loadDiff is an ordinary async function, so cancelling the task only stops things at the await inside the task body. Past that, the abort signal reaches the fetch and nothing else — ApplySearchReplaceBlockTool.execute() awaits a module load with no knowledge of it.
Worth naming the exact hole, because it wasn't where I'd assumed. The catch after getSource already had if (signal.aborted) return, so I'd covered the fetch rejected mid-flight case. What was uncovered was the success path — a fetch that completed before the supersession, followed by an execute() that spans it. That run then resumed and assigned originalCode, modifiedCode, and errorMessage belonging to the newer patch.
Took the second of your two options — verify after every await — rather than moving the work into the task body, because it holds regardless of how ember-concurrency cancels an async task, and I didn't want the correctness of this to rest on my reading of those internals.
let result = await this.cardService.getSource(new URL(fileUrl), { signal });
if (signal.aborted) return;
originalCode = result.status === 404 ? '' : result.content;…and the same guard on both the resolve and reject paths of execute(). One related change your comment implies: the patch is now applied against originalCode — the value this load fetched — instead of this.originalCode, which by then may belong to a different patch entirely.
The destruction case you mention falls out of the same guard, since the destructor aborts the controller.
Added a regression test that forces the ordering deterministically: two loads started with a deferred getSource, the newer one released and allowed to render its diff, then the abandoned one released afterwards. It asserts the diff still shows the newer replacement. Against the previous code the late load overwrites it.
Not verified locally — Colima is down here, so CI is exercising it.
The load runs outside the task, so cancelling the task stops it only at the await inside the task body. Past that point the work carries on regardless, and the abort signal reaches the fetch and nothing else — so a load superseded while awaiting the patch tool resumed later and wrote its answer over the state belonging to the patch that replaced it, putting a stale diff on screen and mutating a resource that may already be gone. Check the signal after each await before writing anything, and apply the patch against the code this load fetched rather than whatever the resource holds by the time it resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The assertion read `view-lines[2]`, copied from a test that renders several editors. This one renders a single diff, so that index held nothing, the predicate was never true, and the wait ran to its timeout instead of failing on anything real. Search every pane for the replacement instead, and wait on the insert decoration the way the neighbouring diff test already does. The abandoned patch's replacement is now asserted absent too, which is the property the test exists to hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading the replacement back out of monaco made the test depend on how the diff editor lays out its panes and which decorations it emits for a one-line change — twice that guess was wrong, and both times it surfaced as a wait running to its timeout rather than as anything about the behaviour under test. Give the abandoned load a search string the file does not contain, so finishing is something it cannot do quietly: it reports that the patch would not apply. The property then reads directly off the absence of that error, and the only DOM this test touches is the diff container and the alert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
richardhjtan
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] I reviewed this as a reactivity fix rather than a rendering one: the interesting questions are what modify now entangles with, whether the abort actually reaches the network (the whole cost argument rests on that), and whether the new early return can leave the resource in a state nothing renders — which is the bug being fixed, so re-introducing a variant of it would be the expensive mistake. Everything below was traced against the checked-out branch.
Bottom line: the diagnosis and the fix are both right, and the two claims the change most depends on hold up under tracing — the abort really does reach the socket, and the early return really is exhaustive. No blocking issues. Five things worth acting on, the first being a latent invalidation loop that is currently unreachable only because of two guards in another file.
What lands right
- The abort reaches the network, which is the claim the "took down staging" half of this rests on.
card-service.getSourcepassessignalintonetwork.authedFetch, which isfetcher(...);fetcherbuildsnew Request(urlOrRequest, init)— carrying the signal — hands that sameRequestobject down the middleware chain unchanged, and callsfetchImplementation(onwardReq.clone())at the bottom, andRequest.clone()preserves the signal. The redirect path insimulateNetworkBehaviorsre-passes the original request as init, so it survives that too. Nothing in the chain reconstructs the request from parts and drops it. - The early return is exhaustive. After
modifyreturns, the resource is in exactly one of three states — loaded, failed, or loading — and every path either sets one or performs a load. That is what makes the new loading branch in the template a genuine fix rather than a fourth state with a spinner on it. - The restart condition compares the right things.
searchReplaceBlockis only populated onceisCompleteSearchReplaceBlockpasses, so it is a stable string after a block completes and the!==compares by value — a rebuiltCodeDatacarrying identical text is correctly not a change. The test pins this. - The ownership guard in the task's
finallyis right about ember-concurrency's ordering. A restartable task cancels the previous instance asynchronously, so a superseded run'sfinallycan land after the newer run has already claimedabortControllerand set the flag. Comparing identity rather than clearing unconditionally is the correct handling, and it is not obvious enough to leave uncommented — good that it isn't. - Applying the patch against the load's own
originalCoderather thanthis.originalCodeis the subtle half of the earlier fix and it is right: the local is the only value guaranteed to belong to the same patch assearchReplaceBlock.
Recommendations
- Add
codePatchStatus === 'applied'to the early-return condition. It is the one exit fromloadDiffthat leaves neither data nor an error, so it is the one input that turns thismodifyinto a self-invalidating loop — currently unreachable only because both consumers happen to exclude it. Mechanism in the thread on that line. Non-blocking, four tokens. - Reword the
loadInFlightcomment:modifydoes consume tracked state two lines below, and the property that makes that safe (every read is on a returning path) is the one worth writing down. Non-blocking. - Add the failing-load arm to the re-render test — a load that errors must also be fetched once and left alone, and that arm is what the second half of the report describes. Non-blocking, cheapest of these.
- The malformed-URL guard stops at
fileName; the same string reachesnew URLagain throughthis.file.sourceUrlin the attached-file menu'sopenInCodeMode. Click-time rather than render-time, so it does not undo the fix. Follow-up. - Drop the always-true
{{#if @codeData.fileUrl}}around the header in the loading branch. Non-blocking.
Adjacent, out of scope
- The
codePatchStatus === 'applied'branch at the top ofloadDiffis dead code as the app stands — no consumer reads this resource while the status isapplied. Its comment is genuinely valuable (it records a deliberate product decision about not re-fetching attached files for big chats), so it should not simply be deleted; but it is currently documentation living in an unreachable branch, and recommendation 1 is what makes that state explicit rather than incidental. getSourcenow takes an options bag that only this caller uses. Fine as it stands; worth knowing that the other call sites get no cancellation, so a slow source fetch elsewhere still runs to completion after its consumer is gone. Not this change's problem, but it is the same class of leak this one just closed.
Generated by Claude Code
| if (!inputsChanged) { | ||
| if (this.isDataLoaded || this.errorMessage || this.loadInFlight) { | ||
| return; | ||
| } | ||
| } else { |
There was a problem hiding this comment.
[Claude Code 🤖] This early return is correct today, and it is one line away from being an infinite invalidation loop. The thing that keeps it safe lives in a different file and is not written down anywhere.
Background — why reading tracked state here is different from reading it anywhere else. ember-modify-based-class-resource's manager builds the resource inside createCache(() => { …; instance.modify(positional, named); return instance }) (dist/core/class-based/manager.js). Everything consumed during that computation — including tracked state read inside modify — entangles with the cache. Dirty it and the next getValue re-runs modify. this.isDataLoaded reads the tracked originalCode and modifiedCode; this.errorMessage is tracked too. So this line entangles all three, and load writes all three.
That combination is safe here for one reason only: every path that reads them returns without writing. Data loaded → return. Error set → return. Load in flight → return. The only way through to load.perform() is with all three falsy, and every terminal state loadDiff can reach sets one of them.
Except one. loadDiff opens with if (codePatchStatus === 'applied'), which sets originalCode = null, modifiedCode = null, and returns — the one exit that leaves neither data nor an error. Reach modify in that state with unchanged inputs and it goes: read isDataLoaded (false) → read errorMessage (null) → load.perform(). Ember-concurrency starts a task instance synchronously on perform, and loadDiff's applied branch has no await before its return, so those two writes land inside the same cache computation that just read them. In a debug build that is the backtracking assertion; in production it is the cache invalidating itself on every read.
Why it doesn't fire today. I traced every consumer of this resource before writing the above. There are exactly two, both in aibot-message.gts, and both exclude applied:
codePatchErrorMessagereturnsnullat its first branch when the status isapplied, before it touchesthis.codeDiffResource.- The template reads
codeDiffResource.isDataLoaded/isLoadingDiffonly inside the{{else}}of{{#if this.isAppliedOrIgnoredCodePatch}}, and that getter iscodePatchStatus === 'applied' || !isLastAssistantMessage.
So modify is never called while the status is applied, the branch at loadDiff's top is unreachable from here, and the loop cannot start. That is a real invariant, held entirely by two guards in a component two files away, with nothing in this file recording it.
The fix is one clause, and it also makes the dead branch in loadDiff honest:
| if (!inputsChanged) { | |
| if (this.isDataLoaded || this.errorMessage || this.loadInFlight) { | |
| return; | |
| } | |
| } else { | |
| if (!inputsChanged) { | |
| if ( | |
| this.isDataLoaded || | |
| this.errorMessage || | |
| this.loadInFlight || | |
| codePatchStatus === 'applied' | |
| ) { | |
| return; | |
| } |
With that, applied is a terminal state of modify rather than a state load is asked to reach and then abandon, and the guard stops depending on a consumer's branch structure. The codePatchStatus === 'applied' block in loadDiff can then either go or stay as documentation — but it should not be the thing standing between this and a loop.
Scope. Latent, not live — nothing in the current app reaches it. Non-blocking, but it costs four tokens to remove and the failure mode if someone later renders a loading state for an applied patch is a render loop, not a wrong pixel.
There was a problem hiding this comment.
[Claude Code 🤖] Taken, in ba5f454 — and the trace is right down to the synchronous write, which is the part that makes it a loop rather than a stale read.
I confirmed the reachability argument independently before changing anything: codePatchErrorMessage returns null at its first branch on applied, before touching the resource, and the template reads isDataLoaded / isLoadingDiff only inside the {{else}} of isAppliedOrIgnoredCodePatch. Resources are pull-based, so with no consumer reading it while applied, modify doesn't run and the branch is unreachable. Latent, as you say.
Worth naming the shape precisely, because it needs two calls: the first with applied has appliedStateChanged true, so it takes the inputsChanged branch and never reads the tracked state. It's the second call — same status, inputs unchanged — that reads all three, finds them falsy because the first load nulled them, and performs. That's the read-then-write inside one computation.
I ordered the new clause first rather than last:
if (
codePatchStatus === 'applied' ||
this.isDataLoaded ||
this.errorMessage ||
this.loadInFlight
) {|| is left-to-right, so this returns without reading tracked state at all in the applied case — the entanglement never forms, instead of forming and then being harmless. Same four tokens.
I left the applied block at the top of loadDiff. With this change it's no longer load-bearing, but it's still reached on that first call and still does the nulling, so removing it would mean moving that clearing into modify — a bigger change than the problem justifies. It now reads as what it always claimed to be: documentation for why an applied patch has no diff to show.
| // Deliberately untracked: `modify` consults this to decide whether a load is | ||
| // already under way, and consuming tracked state there would re-enter | ||
| // `modify` every time the load changed it. The template reads the task's own | ||
| // `isRunning` instead, which is tracked and safe to render from. | ||
| private loadInFlight = false; |
There was a problem hiding this comment.
[Claude Code 🤖] The comment states the right concern and then understates what the code does with it, which will mislead the next reader.
modify does consume tracked state, eight lines below: this.isDataLoaded reads the tracked originalCode and modifiedCode, and this.errorMessage is tracked. Both are written by load. So the resource is entangled with load state whether or not loadInFlight is tracked, and keeping this flag untracked buys something narrower than "we don't consume tracked state in modify" — it keeps a mid-flight signal out of the entanglement, so a load starting and finishing doesn't invalidate the cache twice on its own.
The invariant that actually makes the entanglement safe is worth writing down instead, because it is the one a future edit can break without noticing: every path in modify that reads isDataLoaded or errorMessage returns without writing them. Something like:
// Untracked on purpose. `modify` runs inside the resource's tracked cache,
// so anything it reads re-runs it when written. The load-completion state it
// reads (`isDataLoaded`, `errorMessage`) is only ever read on paths that
// return, so a completed load costs one extra `modify` and stops. A
// mid-flight flag read the same way would add a second invalidation per
// load, and a path that read either one and then started a load would
// re-enter without bound.
Scope. Comment accuracy. Non-blocking — the code is right; the note next to it should describe the rule the code is following, since that rule is the whole safety argument. Related: the thread on the early return below, which is where that rule is closest to being broken.
There was a problem hiding this comment.
[Claude Code 🤖] You're right, and the comment was worse than imprecise — it implied modify doesn't consume tracked state, when it does so eight lines below. Rewritten in ba5f454.
The version I landed states the invariant as the safety argument rather than as a property of the flag:
every path in
modifythat reads those returns without writing them, so a finished load costs one furthermodifyand stops
with the narrower thing the untracked flag actually buys — sparing a second invalidation per load — kept as the secondary clause rather than the headline.
The pairing with the thread below is the useful part: that rule is what makes the entanglement safe, and the applied path was the one place it was about to be broken. Having fixed that, the comment now describes a rule the code actually keeps everywhere, which is the only kind worth writing down.
| let fileUrl = this.fileUrl; | ||
| if (!fileUrl) { | ||
| return ''; | ||
| } | ||
| try { | ||
| return new URL(fileUrl).pathname.split('/').pop() || ''; | ||
| } catch { | ||
| // The model names the file it is patching, and what it writes is not | ||
| // always a URL. Falling back to the last path segment keeps a header that | ||
| // is merely wrong from taking the whole message down with it; whether the | ||
| // patch can be applied is reported separately. | ||
| return fileUrl.split('/').pop() || ''; | ||
| } |
There was a problem hiding this comment.
[Claude Code 🤖] The guard is right, and the same raw string still reaches an unguarded new URL one component away.
Confirmation on this getter. The fallback is the correct shape: new URL('malformed file url') throws, and 'malformed file url'.split('/').pop() gives back the whole string, which is what the model wrote and therefore the most useful thing to show. The test pins exactly that.
Where it doesn't reach. this.file (the @cached getter above) builds its FileDef with sourceUrl: this.sourceUrl ?? '', and sourceUrl returns this.args.codeData.fileUrl verbatim for a non-new file — the same unparseable string. That FileDef goes straight into <AttachedFileDropdownMenu @file={{this.file}} …>, whose openInCodeMode does new URL(this.args.file.sourceUrl!) (packages/host/app/components/ai-assistant/attached-file-dropdown-menu.gts). The '' case is reachable too: sourceUrl returns null for a new file that isn't applied yet, so this.file.sourceUrl is '' and new URL('') throws the same way.
That is a click-time throw inside an @action, not a render-time one, so it does not undo what this change fixes — the message renders, which was the point. But the menu item is offered on a header whose file URL this change has just established may not be a URL, and choosing it throws.
Scope. Follow-up, non-blocking, and arguably pre-existing rather than introduced here — the '' path predates this change. Worth either guarding openInCodeMode the same way, or omitting the "open in code mode" item when sourceUrl doesn't parse, so the header degrades consistently instead of half-way.
There was a problem hiding this comment.
[Claude Code 🤖] The main finding holds and I'm treating it as a follow-up rather than folding it in — but one detail of it doesn't survive checking, and it's the half that would have decided the fix.
Confirmed: openInCodeMode does new URL(this.args.file.sourceUrl!) with no guard, and for a non-new file sourceUrl hands it codeData.fileUrl verbatim. So the unparseable-but-non-empty case — malformed file url, exactly what this change made renderable — is offered as an enabled menu item that throws on click.
Not confirmed: the '' case. The menu item carries disabled: !this.args.file?.sourceUrl (attached-file-dropdown-menu.gts), and '' is falsy, so the new-file-not-yet-applied path you describe is already disabled rather than reachable. That matters for the remedy: "omit the item when sourceUrl doesn't parse" is the right shape, but it's an extension of a guard that exists, not a missing guard — the existing one just tests presence where it needs to test parseability.
Why not here. It's a different component, a click-time throw rather than a render-time one, and the fix wants its own test around the menu rather than the header. This branch has had a long CI road and I'd rather not widen it for something no current path reaches on render. Happy to do it as a follow-up — disabled: !parses(this.args.file?.sourceUrl) with a small helper is the whole change.
Agreed on the framing though: this change established that a header's file URL may not be a URL, so anything downstream taking that string as one is now on notice.
| {{#if @codeData.fileUrl}} | ||
| <codeBlock.diffEditorHeader | ||
| @codeData={{@codeData}} | ||
| @diffEditorStats={{null}} | ||
| @originalUploadedFileUrl={{@codePatchResult.originalUploadedFileUrl}} | ||
| @codePatchStatus={{@codePatchStatus}} | ||
| @userMessageThisMessageIsRespondingTo={{@userMessageThisMessageIsRespondingTo}} | ||
| @codePatchErrorMessage={{this.codePatchErrorMessage}} | ||
| /> | ||
| {{/if}} |
There was a problem hiding this comment.
[Claude Code 🤖] Non-blocking, two small things about this branch.
This {{#if}} is always true. isLoadingDiff requires this.load.isRunning, and modify performs the load only after if (!fileUrl) { this.errorMessage = 'Missing file URL in the code block'; return; }. So a running load implies a file URL, and the guard can go — one less branch to reason about in a template that already nests four deep. If it is here defensively, a comment saying so would keep the next reader from hunting for the case that needs it.
The state this branch exists to eliminate. Worth recording that it is now exhaustive, because that is the load-bearing property: after modify returns, the resource is always in exactly one of loaded (isDataLoaded), failed (errorMessage), or loading (load.isRunning) — the early return above only skips perform when one of the first two already holds, and every other path performs. The blank block this change removes was the fourth state, and there is no longer a way to reach it. That is the sentence I would want in the comment above rather than the rationale for the header, which is self-evident from the markup.
There was a problem hiding this comment.
[Claude Code 🤖] Both taken in ba5f454.
The guard is gone. Your reasoning checks out: modify reaches load.perform() only past if (!fileUrl) { this.errorMessage = …; return; }, so a running load implies a file URL and isLoadingDiff requires load.isRunning. It wasn't defensive, just redundant — and one less branch in a template already nesting four deep is worth having. Nothing crashes if the invariant were ever broken either, since fileName now falls back rather than constructing a URL blindly.
The comment says the exhaustiveness now. That is the better content, and I'd missed that it was the point:
Together with the branch above, this makes the states a patch can be rendered in exhaustive: once
modifyreturns, the resource holds code, or an error, or a running load. There is no fourth state, and the empty block this branch replaced was it.
The header rationale is down to one clause explaining why it can always name its file, which is the only part not evident from the markup.
Worth noting these two threads compose: with applied now returning from modify, "code, error, or running load" is exhaustive over every path modify can take, not just the ones a consumer currently reaches.
| for (let i = 0; i < 5; i++) { | ||
| component.htmlParts = parseHtmlContent(codeBlockHtml, roomId, eventId); | ||
| await settled(); | ||
| } | ||
|
|
||
| assert.strictEqual( | ||
| getSourceCallCount, | ||
| 1, | ||
| 'unchanged inputs do not send the realm another request', | ||
| ); | ||
| assert.dom('.code-block-diff').exists('the diff survives re-rendering'); |
There was a problem hiding this comment.
[Claude Code 🤖] Strong test — the fresh-CodeData-with-identical-values setup is the right reproduction, and I checked that it exercises what it claims: searchReplaceBlock is only populated once isCompleteSearchReplaceBlock passes (packages/host/app/lib/formatted-message/utils.ts), so after a block completes the same string arrives on every re-parse and this.searchReplaceBlock !== searchReplaceBlock is false by value comparison. The whole fix rests on that being a string rather than a rebuilt object, and this test would fail the moment it stopped being one.
The arm that isn't covered is the one from the other half of the report. The description names two symptoms: the block sat holding no code, and the error was cleared along with it. This test pins the success arm of the early return — isDataLoaded → don't refetch. The errorMessage arm has no equivalent, and it is the arm that governs a failing load: a file that 404s or a realm that errors must be fetched once and then left alone, with the message still on screen after N re-renders. Right now, deleting this.errorMessage from the early-return condition leaves every test in this file green while restoring a request-per-invalidation storm against exactly the URLs that were failing — which is the shape that took staging down.
It is a small addition on top of what is already here:
cardService.getSource = async () => {
getSourceCallCount++;
throw new Error('realm unavailable');
};
// …render, then re-render with identical parts N times…
assert.strictEqual(getSourceCallCount, 1, 'a failed load is not retried on every invalidation');
assert.dom('[data-test-error-message]').exists('and its message survives');Scope. Test coverage, non-blocking — but this is the cheapest of the suggestions here and it guards the more expensive failure.
There was a problem hiding this comment.
[Claude Code 🤖] Added in b888ce8 — you were right that this was the cheapest suggestion guarding the more expensive failure, so I did it first.
The test fetches once, fails, then re-renders five times with identical parts and asserts both halves: getSourceCallCount still 1, and [data-test-error-message] still on screen. Deleting this.errorMessage from the early-return condition now fails it, which was the gap — before this, that deletion was green.
Your point about why the existing test works is the part I'd want kept: searchReplaceBlock is a string populated only once isCompleteSearchReplaceBlock passes, so identical re-parses compare equal by value and fileOrPatchChanged stays false. The whole fix rests on that, and if it ever became a rebuilt object both tests would fail rather than silently stop testing anything — which is the right failure direction.
One caveat on the new test, stated plainly: it is typechecked and linted but unrun. Colima is down on this machine so the host suite can't start locally, and CI is the first thing to exercise it. Earlier in this branch's life two monaco-dependent tests passed review by reading and then failed on CI, so I've kept this one off the diff editor entirely — it asserts on the error alert and a call counter, nothing that depends on how monaco lays out panes.
`modify` runs inside the resource's tracked cache, so what it reads there re-runs it when written — and it reads load state. That is safe only because every path reading it returns without writing. One state slipped through: a load asked to run while the patch is already applied clears both codes and returns, recording neither data nor an error, and it does so synchronously. Reaching it left `modify` writing the state it had just read, from inside the computation that read it — a cache dirtying itself on every read. Nothing in the app gets there today; two guards in a component two files away are what stand in the way, and neither is visible from here. Return on `applied` instead, so it is a state `modify` settles in rather than one a load is sent to abandon. State the entanglement invariant next to the flag that depends on it, since an edit can break it without looking wrong. In the template, drop the file-URL check around the loading header: a load only runs once a URL is known, because `modify` records an error and returns without performing when there isn't one. Say instead what that branch is for — with it, the states a patch renders in are exhaustive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existing test pins the loaded arm: once a diff is in hand, re-rendering does not fetch again. The failing arm had nothing on it, and it is the arm that matters more — dropping `errorMessage` from the condition left every test in the file green while restoring a request per invalidation against exactly the URL that was already failing, and taking the message off screen with it. Fetch once, fail, then re-render with identical parts and assert both: no second request, and the error still shown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I’ve been seeing gaps like this:
it comes with an infinite series of 404s, which actually managed to take down staging:
Claude’s explanation
A code block's arguments are recomputed on every invalidation of the room resource, which during streaming arrives continuously. `modify` treated each one as a reason to start over: it cleared any error and performed the load again, and because that load is a restartable task, performing it cancelled the request in flight.Two things followed. The resource sat holding no code and no error, which is the one state the template renders as nothing at all — a code block with a header, an editor and an apply button in the DOM but no content in any of them. And since cancelling the task does not cancel the request behind it, every restart left its fetch running and started another, so a patch creating a new file — where a 404 is the expected answer — asked the realm for that file thousands of times. The requests then contended with each other, so each load took longer, so more of them were cancelled before they could finish.
Restart only when the file or the patch actually changes; otherwise leave a load that is already under way alone. Give the load an abort signal so a superseded one stops rather than merely having its answer discarded, and thread that signal through getSource. Keep the in-flight flag untracked, since consuming tracked state in
modifywould re-enter it on every change.